package com.panopset;

import static com.panopset.Util.getCanonicalPath;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.text.JTextComponent;

import com.panopset.flywheel.Flywheel;
import com.panopset.gui.FlexFrame;
import com.panopset.gui.Osgw;
import com.panopset.gui.TPanel;
import com.panopset.gui.TypicalDialog;
import com.panopset.io.DirectoryProcessor;
import com.panopset.io.FileProcessor;

/**
 * General static utilities.
 *
 * @author Karl Dinwiddie
 */
public final class Util {

    public static final long VERSION_SERIAL_NUMBER = 1L;
    /**
     * @deprecated use "";
     */
    public static final String EMPTY_STRING = "";
    public static final int MIN_JAVA_VERS = 6;
    public static final int CONNECT_TIMEOUT = 25000;
    public static final String CONN_TIMEOUT_STR = "" + CONNECT_TIMEOUT;
    public static Toolkit TOOLKIT = Toolkit.getDefaultToolkit();
    public static final Insets TYPISCHE_INSETS = new Insets(4, 4, 4, 4);
    public static final Color CLR_SUNRISE_ORANGE = new Color(255, 204, 102);
    public static final Color CLR_LIGHT_PURPLE = new Color(255, 200, 240);
    public static final Color CLR_SANDY_BEACH = new Color(255, 255, 180);
    public static final Color CLR_DARK_RED = new Color(102, 0, 0);
    public static final Color CLR_DEEP_RED = new Color(0x55, 0, 0);
    public static final Color DARK_BURGUNDY = new Color(0xdd, 0x00, 0x00);
    public static final String ARIAL = "Arial";
    public static final String DIALOG = "Dialog";
    public static final String LUCIDA_SANS = "Lucida Sans";
    public static final Font MONOSPACE_PLAIN_10 = new Font(Font.MONOSPACED,
            Font.PLAIN, 10);
    public static final Font MONOSPACE_PLAIN_12 = new Font(Font.MONOSPACED,
            Font.PLAIN, 12);

    ;
    public static final Font MONOSPACE_BOLD_10 = new Font(Font.MONOSPACED,
            Font.BOLD, 10);
    public static final Font MONOSPACE_BOLD_12 = new Font(Font.MONOSPACED,
            Font.BOLD, 12);
    public static final Font MONOSPACE_PLAIN_24 = new Font(Font.MONOSPACED,
            Font.PLAIN, 24);
    public static final Font MONOSPACE_BOLD_24 = new Font(Font.MONOSPACED,
            Font.BOLD, 24);
    public static final Font ARIAL_PLAIN_9 = new Font(ARIAL, Font.PLAIN, 9);
    public static final Font ARIAL_PLAIN_10 = new Font(ARIAL, Font.PLAIN, 10);
    public static final Font ARIAL_BOLD_10 = new Font(ARIAL, Font.BOLD, 10);
    public static final Font ARIAL_BOLD_12 = new Font(ARIAL, Font.BOLD, 12);
    public static final Font ARIAL_ITALIC_12 = new Font(ARIAL, Font.ITALIC, 12);
    public static final Font ARIAL_BOLD_14 = new Font(ARIAL, Font.BOLD, 14);
    public static final Font ARIAL_PLAIN_12 = new Font(ARIAL, Font.PLAIN, 12);
    public static final Font DIALOG_PLAIN_12 = new Font(DIALOG, Font.PLAIN, 12);
    public static final Font DIALOG_BOLD_16 = new Font(DIALOG, Font.BOLD, 16);
    public static final Font DIALOG_PLAIN_18 = new Font(DIALOG, Font.PLAIN, 18);
    public static final Font DIALOG_BOLD_18 = new Font(DIALOG, Font.BOLD, 18);
    public static final Font LUCIDA_SANS_10 = new Font(LUCIDA_SANS, 1, 10);
    public static final Font LUCIDA_SANS_14 = new Font(LUCIDA_SANS, 1, 14);
    public static final Font LUCIDA_SANS_12 = new Font(LUCIDA_SANS, 1, 12);
    public static final Font LUCIDA_SANS_18 = new Font(LUCIDA_SANS, 1, 18);
    public static final int MINUTE = 60;
    public static final int HOUR = MINUTE * 60;
    public static final int DAY = HOUR * 24;
    public static final SimpleDateFormat TIMESTAMP_SECONDS = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");
    public static final SimpleDateFormat TIMESTAMP = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss.SSS");
    public static final SimpleDateFormat TIMESHORT = new SimpleDateFormat(
            "yyyyMMdd");
    /** MMMMMMMMMMM dd, yyyy */
    public static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat(
            "MMMMMMMMMMM d, yyyy");
    /** HH:mm:ss MMMMMMMMMMM dd, yyyy */
    public static final SimpleDateFormat CLOCK_FORMAT = new SimpleDateFormat(
            "KK:mm:ss aa MMMMMMMMMMM dd, yyyy ");
    /** HH:mm:ss MMMMMMMMMMM dd, yyyy */
    public static final SimpleDateFormat VERSION_FORMAT = new SimpleDateFormat(
            "yyyyMMdd");
    /** EEEE, MMMM dd, yyyy, h:mm a(zz) */
    public static final SimpleDateFormat LAST_MODIFIED_FORMAT = new SimpleDateFormat(
            "EEEE, MMMM dd, yyyy, h:mm a(zz)");
    public static final String OPEN_JSP = "<%";
    public static final String CLOSE_JSP = "%>";
    public static final String DEFAULT_DIRECTIVE = "@";
    public static final String DEFAULT_OPEN_DIRECTIVE = "${";
    public static final String DEFAULT_CLOSE_DIRECTIVE = "}";
    public static final String OPEN_PAREN = "(";
    public static final String CLOSE_PAREN = ")";
    public static final String BACK_SLASH = "\\";
    public static final String FORWARD_SLASH = "/";
    public static final String SINGLE_SPACE = " ";
    public static final String DOUBLE_SPACE = "  ";
    public static final String TRIPLE_SPACE = "   ";
    public static final String PERIOD = ".";
    public static final String PIPE = "|";
    public static final String HASH = "#";
    public static final String BANG = "!";
    public static final String TILDE = "~";
    public static final String DASHER = " - ";
    public static final String UNDERSCORE = "_";
    public static final String QUESTION_MARK = "?";
    public static final String COMMA = ",";
    public static final String COLON = ":";
    public static final String UTF_8 = "UTF8";
    public static final String ZERO = "0";
    public static final String PATH_SEP = System.getProperty("file.separator");
    public static final String USER_DIR = System.getProperty("user.dir");
    public static final String DOS_RTN = "" + (char) 13 + (char) 10;
    public static final String RTN = System.getProperty("line.separator");
    public static final String USER_HOME = System.getProperty("user.home");
    public static final String PANSOPSET_USER_DIR = USER_HOME + PATH_SEP
            + ".com.panopset";
    public static final String EQUALS = "=";
    public static final String FAVICON = "/favicon.ico";
    public static final String USERHOME = "user.home";
    public static final String TAB = EMPTY_STRING + '\u0009';
    public static final String XML_SFX = ".xml";
    public static final String JAVA_RTN = "\n";
    public static final String COPYRIGHT = "\u00a9";
    public static final long ONE_SECOND = 1000;
    public static final long ONE_MINUTE = ONE_SECOND * 60;
    public static final long ONE_HOUR = ONE_MINUTE * 60;
    public static final long ONE_DAY = ONE_HOUR * 24;
    public static final long ONE_WEEK = ONE_DAY * 7;

    public static void setParentFrame(FlexFrame frame) {
        parentFrame = frame;
    }

    public static Object loadObject(Class<?> clazz, File file) {
        return UtilIO.loadObject(clazz, file);
    }

    public static void saveObject(Object object, File file) {
        UtilIO.saveObject(object, file);
    }

    private static String eol;

    /**
     * Defaults to System.getProperty("line.separator")
     *
     * @return Default value or value set by setEol.
     */
    public static String getEol() {
        if (eol == null) {
            eol = RTN;
        }
        return eol;
    }

    /**
     * Rarely used.
     *
     * @param value
     */
    public static void setEol(String value) {
        eol = value;
    }

    /**
     * Clear all text components, including those in parent containers.
     *
     * @param c
     *            Container to have all editable text fields cleared for.
     */
    public static void clearAllTextComponents(Container c) {
        if (c == null) {
            return;
        }
        Container p = c.getParent();
        while (p != null && p != c) {
            clearAllTextComponents(p);
            return;
        }
        clearAllSubText(c);
    }

    /**
     * Clear all text components in a container.
     *
     * @param c
     *            Container for which all text components are to be cleared.
     */
    public static void clearAllSubText(Container c) {
        for (Component s : c.getComponents()) {
            if (s instanceof JTextComponent) {
                JTextComponent t = (JTextComponent) s;
                if (t.isEditable()) {
                    t.setText(EMPTY_STRING);
                }
            } else if (s instanceof Container) {
                clearAllSubText((Container) s);
            }
        }
    }

    /**
     *
     * @param imageURLpath
     * @param targetDir
     * @return a href for image.
     */
    public static String createImageLinkFromURL(String imageURLpath,
            String targetDir) {
        return "";
    }

    /**
     * The target directory is the Flywheel target directory or the home
     * directory, depending on whether or not this was executed from a Flywheel
     * script.
     *
     * @param imageURLpath
     * @return
     */
    public static String createImageLinkFromURL(String imageURLpath) {
        return "";
    }

    /**
     * @return true if verbose messages flag is true, default is false.
     */
    public static boolean isVerbose() {
        return verbose;
    }

    /**
     * Used by vlog, iff true, vlog will log a message.
     */
    public static boolean verbose = false;

    /**
     * Log only if verbose set to true.
     *
     * @param msg
     */
    public static void vlog(String msg) {
        if (verbose) {
            log(msg);
        }
    }

    /**
     * Return contents of a file as a String.
     *
     * @param fileName
     * @return String full text of file.
     */
    public static String getFileText(String fileName) {
        return getFileText(new File(fileName));
    }

    /**
     * Return contents of a file as a String.
     *
     * @param file
     * @return String full text of file.
     */
    public static String getFileText(File file) {
        StringWriter rtn = new StringWriter();
        boolean firstTime = true;
        if (!file.exists()) {
            return EMPTY_STRING;
        }
        for (String s : loadFile(file)) {
            if (firstTime) {
                firstTime = false;
            } else {
                rtn.append(getReturnChar());
            }
            rtn.append(s);
        }
        return rtn.toString();
    }

    public static String loadTextFromResource(String resourcePath) {
        StringWriter sw = new StringWriter();
        try {
            for (String s : loadLinesFromResource(resourcePath)) {
                sw.append(s);
                sw.append(getReturnChar());
            }
        } catch (IOException e) {
            log(e);
        }
        return sw.toString();
    }

    public static Vector<String> loadLinesFromResource(String resourcePath)
            throws IOException {
        return UtilIO.loadLinesFromResource(resourcePath);
    }

    public static BufferedInputStream getResourceStream(String resourcePath) {
        return UtilIO.getResourceStream(resourcePath);
    }

    /**
     * Load a file into an array. This should be done with reasonable sized text
     * files.
     *
     * @param file
     * @return array
     */
    public static Vector<String> loadFile(File file) {
        return UtilIO.loadFile(file);
    }

    /**
     * Convert a Vector<String> to an array.
     *
     * @param vector
     * @return array
     */
    public static String[] toArray(Vector<String> vector) {
        String[] rtn = new String[vector.size()];
        int i = 0;
        for (String s : vector) {
            rtn[i++] = s;
        }
        return rtn;
    }

    /**
     * Copy one String map to another.
     *
     * @param fromMap
     *            Map to copy from.
     * @param toMap
     *            Map to copy to. Existing entries are kept or written over.
     * @return toMap if toMap is null, a new map will be created.
     */
    public static Map<String, String> copyMap(Map<String, String> fromMap,
            Map<String, String> toMap) {
        if (toMap == null) {
            toMap = new HashMap<String, String>();
        }
        if (fromMap != null) {
            for (Entry<String, String> entry : fromMap.entrySet()) {
                toMap.put(entry.getKey(), entry.getValue());
            }
        }
        return toMap;
    }

    public static void delete(String path) {
        if (path == null) {
            return;
        }
        delete(new File(path));
    }

    public static void delete(File path) {
        if (path == null) {
            return;
        }
        if (!path.exists()) {
            return;
        }
        if (!path.delete()) {
            dspmsg(x("Failed to delete") + SINGLE_SPACE
                    + getCanonicalPath(path));
        }
    }

    public static void createNewFile(String path) {
        if (path == null) {
            return;
        }
        createNewFile(new File(path));
    }

    public static void createNewFile(File path) {
        if (path == null) {
            return;
        }
        try {
            if (!path.createNewFile()) {
                dspmsg(x("Failed to create") + SINGLE_SPACE
                        + getCanonicalPath(path));
            }
        } catch (IOException e) {
            log(e);
        }
    }

    public static void mkdirs(String path) {
        if (path == null) {
            return;
        }
        mkdirs(new File(path));
    }

    public static void mkdirs(File path) {
        if (path == null) {
            return;
        }
        if (path.exists()) {
            return;
        }
        if (!path.mkdirs()) {
            dspmsg(x("Unable to create path to") + SINGLE_SPACE
                    + getCanonicalPath(path));
        }
    }

    /**
     * Write a String to a file
     *
     * @param str
     *            String to write
     * @param file
     *            File to write to
     */
    public static void saveStringToFile(String str, File file) {
        UtilIO.saveStringToFile(str, file);
    }

    /**
     * This method will return true if the files are identical, ignoring the
     * kind of return character used.
     *
     * @param one
     *            First file to compare.
     * @param two
     *            Second file to compare.
     * @return true Only if both files exist and are identical.
     */
    public static boolean filesAreSame(final File one, final File two) {
        final boolean[] rtn = new boolean[1];
        rtn[0] = false;
        new FileProcessor.Builder().file(one).file(two).listener(
                new FileProcessor.Listener() {

                    @Override
                    public void finish(FileProcessor processor) {
                        rtn[0] = true;
                        processor.stop();
                    }

                    @Override
                    public void process(FileProcessor processor, String[] s) {
                        if (s.length != 2 || s[0] == null || s[1] == null
                                || !s[0].equals(s[1])) {
                            processor.stop();
                            return;
                        }
                    }
                }).construct().exec();
        return rtn[0];
    }

    /**
     * Verbose log.
     *
     * @param msg
     *            Message to log if verbose is true.
     */
    public static void logv(String msg) {
        if (verbose) {
            log(msg);
        }
    }

    public static final StringWriter logWriter = new StringWriter();

    /**
     * Log a String
     *
     * @param msg
     *            Message to log.
     */
    public static void log(String msg) {
        logWriter.append(msg).append(RTN);
        System.out.println(msg);
    }

    /**
     * Log a Throwable stack trace.
     *
     * @param t
     *            Throwable
     */
    public static void log(Throwable t) {
        Alert.red(t.getMessage());
        log("*************************");
        log(getStackTrace(t));
        // if we hit any kind of exception,
        // halt all animation threads.
        Osgw.close();
    }

    /**
     * Display a message.
     *
     * @param msg
     *            Message to display.
     */
    public static void dspmsg(String msg) {
        Alert.clearAll();
        Alert.green(msg);
    }

    /**
     * Clear messages.
     */
    public static void clrmsg() {
        Alert.clearAll();
    }

    /**
     * Wrapper for java.io.File.getCanonicalPath method, to handle IOException.
     * If an IOException is caught, the exception is logged by the log function,
     * and the results of the Exception.getMessage function are returned.
     *
     * @param f
     *            File to get canonical path of.
     * @return result of java.io.File.getCanonicalPath
     */
    public static String getCanonicalPath(final File f) {
        try {
            return f == null ? EMPTY_STRING : f.getCanonicalPath();
        } catch (IOException e) {
            log(e);
            return e.getMessage();
        }
    }

    /**
     * Get the parent directory, fully qualified even if the file is based
     * on a relative path.
     * @param f
     * @return
     */
    public static String getParentDirectory(final File f) {
        if (f.exists()) {
            File fullFile = new File(getCanonicalPath(f));
            return getCanonicalPath(fullFile.getParentFile());
        }
        return getCanonicalPath(f.getParentFile());
    }

    /**
     * Stub class for future Nation Language System use, doesn't do anything.
     *
     * @param s
     * @return s
     */
    public static String x(String s) {
        return s;
    }

    /**
     * Generic dump. Collections and dumps are nicely formatted.
     *
     * @param o
     *            Object to be given a clean dump for.
     * @return String representation of given Object.
     */
    @SuppressWarnings("unchecked")
    public static String dump(final Object o) {
        StringWriter sw = new StringWriter();
        if (o == null) {
            return x("Can not dump null object" + PERIOD);
        }
        if (o instanceof Object[]) {
            for (Object s : (Object[]) o) {
                sw.append(dump(s));
            }
        } else if (o instanceof Map) {
            Map<Object, Object> m = (Map<Object, Object>) o;
            for (Entry<Object, Object> entry : m.entrySet()) {
                sw.append(entry.getKey().toString());
                sw.append(getReturnChar());
                Object v = entry.getValue();
                if (v == null) {
                    sw.append("null");
                } else {
                    sw.append(v.toString());
                }
                sw.append(getReturnChar());
                sw.append(getReturnChar());
            }
        } else if (o instanceof Collection) {
            for (Object e : (Collection) o) {
                sw.append(dump(e));
            }
        } else {
            sw.append(o.toString());
            sw.append(getReturnChar());
        }
        return sw.toString();
    }

    /**
     * Invoke a static method.
     * <ul>
     * <li>It must return a String</li>
     * </ul>
     *
     * <pre>
     * &#064;param classMethodAndParms
     * Example:
     *
     * <pre>
     * com.panopset.Util.capitalize(foo)
     *
     * </pre>
     *
     * @return String result of method invocation.
     */
    public static String invokeStaticStringMethod(String classMethodAndParms)
            throws Exception {
        return invokeStaticStringMethod(classMethodAndParms, null);
    }

    /**
     * Invoke a static method.
     * <ul>
     * <li>It must return a String</li>
     * <li>The parameters are keys to a map.</li>
     * </ul>
     *
     * <pre>
     * &#064;param classMethodAndParms
     * Example:
     *
     * <pre>
     * com.panopset.Util.capitalize(foo)
     *
     * Note that if foo is not in the map, the value passed will be foo
     * itself.
     * </pre>
     *
     * @param mapProvider
     *            Can be null if there are no parameters.
     * @return String result of invoking method.
     */
    public static String invokeStaticStringMethod(String classMethodAndParms,
            MapProvider mapProvider) throws Exception {
        return new ReflectionInvoker.Builder().classMethodAndParms(
                classMethodAndParms).mapProvider(mapProvider).construct()
                .exec();
    }

    private static String returnChar;

    /**
     * Set global static custom return character. Rarely used.
     *
     * @param s
     */
    public static void setReturnChar(String s) {
        returnChar = s;
    }

    /**
     * Default is cr lf.
     *
     * @return String return character(s).
     */
    public static String getReturnChar() {
        if (returnChar == null) {
            returnChar = DOS_RTN;
        }
        return returnChar;
    }

    /**
     * Capitalize first letter in a String.
     *
     * @param s
     *            String to capitalize
     * @return Capitalized value.
     */
    public static String capitalize(final String s) {
        return s == null ? EMPTY_STRING : s.length() > 0 ? s.length() == 1 ? s
                .toUpperCase() : (EMPTY_STRING + s.charAt(0)).toUpperCase()
                + s.substring(1) : EMPTY_STRING;
    }

    /**
     * String.toUpperCase wrapper
     *
     * @param s
     * @return
     */
    public static String toUpperCase(final String s) {
        return s.toUpperCase();
    }

    /**
     * Do a global replacement on all files in a directory, and its
     * sub-directories.
     *
     * @param directory
     * @param from
     * @param to
     */
    public static void globalReplace(File directory, final String from,
            final String to) {
        new DirectoryProcessor(directory) {

            @Override
            protected void process(final File f) {

                new FileProcessor.Builder().file(f).listener(
                        new FileProcessor.Listener() {

                            Vector<String> v = new Vector<String>();

                            @Override
                            public void finish(FileProcessor processor) {
                                StringBuffer sb = new StringBuffer();
                                boolean firstTime = true;
                                for (String s : v) {
                                    if (firstTime) {
                                        firstTime = false;
                                    } else {
                                        sb.append(getReturnChar());
                                    }
                                    sb.append(s);
                                }
                                saveStringToFile(sb.toString(), f);
                            }

                            @Override
                            public void process(FileProcessor processor,
                                    String[] s) {
                                for (String t : s) {
                                    v.add(t.replace(from, to));
                                }
                            }
                        }).construct().exec();
            }
        }.exec();
    }

    /**
     * Get the keys of a text representation of a properties file. Line values
     * are expected to be separated by an '=' sign.
     *
     * @param s
     *            String to parse in to lines.
     * @return Vector<String> of keys
     */
    public static Vector<String> stringLineKeys(String s) {
        return stringLineKeys(s, EQUALS);
    }

    /**
     * Get the keys of a text representation of a properties file. Rarely used,
     * typically the separator is '=' and the stringLineKeys(String) method
     * would be used.
     *
     * @param s
     *            String to parse in to lines.
     * @param separator
     * @return Vector<String> of keys
     */
    public static Vector<String> stringLineKeys(String s, String separator) {
        Vector<String> v = new Vector<String>();
        if (isPopulated(s)) {
            StringReader sr = new StringReader(s);
            BufferedReader br = new BufferedReader(sr);
            try {
                String rs = br.readLine();
                while (rs != null) {
                    v.add(getKeyFromStringRep(rs, separator));
                    rs = br.readLine();
                }
            } catch (IOException ex) {
                log(ex);
            }
        }
        return v;
    }

    /**
     * @param s
     * @return Count of lines in s.
     */
    public static int lineCount(String s) {
        if (isPopulated(s)) {
            Vector<String> v = stringLines(s);
            if (v != null) {
                return v.size();
            }
        }
        return -1;
    }

    /**
     * @param s
     * @return Count of lines in s, not including blank lines.
     */
    public static int lineCountSkipBlanks(String s) {
        if (isPopulated(s)) {
            Vector<String> v = stringLinesSkipBlanks(s);
            if (v != null) {
                return v.size();
            }
        }
        return -1;
    }

    /**
     * Convert a String to a Vector<String> separated by return characters.
     *
     * @param s
     *            String to parse in to lines.
     * @return Vector<String> of lines of text.
     */
    public static Vector<String> stringLines(String s) {
        Vector<String> v = new Vector<String>();
        if (isPopulated(s)) {
            StringReader sr = new StringReader(s);
            BufferedReader br = new BufferedReader(sr);
            try {
                String rs = br.readLine();
                while (rs != null) {
                    v.add(rs);
                    rs = br.readLine();
                }
            } catch (IOException ex) {
                log(ex);
            }
        }
        return v;
    }

    /**
     * Convert a String to a Vector<String> separated by return characters.
     * Ignore empty lines.
     *
     * @param s
     *            String to parse in to lines.
     * @return Vector<String> of lines of text.
     */
    public static Vector<String> stringLinesSkipBlanks(String s) {
        Vector<String> v = new Vector<String>();
        if (isPopulated(s)) {
            StringReader sr = new StringReader(s);
            BufferedReader br = new BufferedReader(sr);
            try {
                String rs = br.readLine();
                while (rs != null) {
                    if (isPopulated(rs.trim())) {
                        v.add(rs);
                    }
                    rs = br.readLine();
                }
            } catch (IOException ex) {
                log(ex);
            }
        }
        return v;
    }

    /**
     * Load Properties from a file.
     *
     * @param f
     *            File to load property values from.
     * @return new Properties object.
     */
    public static Map<String, String> loadPropsFromFile(final File f) {
        Properties p = new Properties();
        loadProperties(p, f);
        return loadMapFromProperties(p);
    }

    /**
     * Load Map<String, String> from Properties
     *
     * @param p
     *            Properties to load Map<String, String> from.
     * @return Map<String, String>
     */
    public static Map<String, String> loadMapFromProperties(final Properties p) {
        final Map<String, String> rtn = new HashMap<String, String>();
        for (Object key : p.keySet()) {
            rtn.put(key.toString(), p.getProperty(key.toString()));
        }
        return rtn;
    }

    /**
     * Load properties from a file.
     *
     * @param p
     *            Properties to load values in to.
     * @param f
     *            File to load properties from.
     */
    public static void loadProperties(Properties p, File f) {
        if (!f.exists()) {
            log(x("Skipping properties load, file ") + getCanonicalPath(f));
            return;
        }
        try {
            FileReader fr = new FileReader(f);
            BufferedReader br = new BufferedReader(fr);
            p.load(br);
            br.close();
            fr.close();
        } catch (IOException e) {
            log(e);
        }
    }

    /**
     * Save properties to a file.
     *
     * @param p
     *            Properties to save to file.
     * @param f
     *            File to store properties.
     */
    public static void saveProperties(Properties p, File f) {
        try {
            FileOutputStream fos = new FileOutputStream(f);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            p.store(bos, new Date().toString());
            bos.flush();
            bos.close();
            fos.close();
        } catch (ClassCastException e) {
            dump(p);
            log(e);
        } catch (IOException e) {
            log(e);
        }
    }

    public static SortedSet<Object> sort(Set<Object> set) {
        SortedSet<Object> ss = Collections
                .synchronizedSortedSet(new TreeSet<Object>());
        for (Object o : set) {
            ss.add(o);
        }
        return ss;
    }

    /**
     * This is a good example of how the <b>e</b> exec command is useful.
     * Anywhere that &lt;%e getCurrentDateTime%&gt; appears, the output will be
     * replaced with a nice time stamp. Useful for automatically putting a
     * "last modified" time on your HTML page.
     *
     * @see com.panopset.flywheel.CommandExecute
     * @see com.panopset.Commons#CLOCK_FORMAT
     * @return String the nicely formatted time stamp.
     */
    public static String getCurrentDateTime() {
        return CLOCK_FORMAT.format(new Date());
    }

    /**
     * This is a another good example of how the <b>e</b> exec command is
     * useful. Use this to set the last-modified header meta tag.
     *
     * @see com.panopset.flywheel.CommandExecute
     * @see com.panopset.Commons#LAST_MODIFIED_FORMAT
     * @return String current date formatted for html meta tag last-modified.
     */
    public static String getLastModifiedDate() {
        return LAST_MODIFIED_FORMAT.format(new Date());
    }

    /**
     * @see com.panopset.flywheel.CommandExecute
     * @see com.panopset.Commons#TIMESTAMP_FORMAT
     */
    public static String getCurrentDate() {
        return TIMESTAMP_FORMAT.format(new Date());
    }

    /**
     * If s1 is equal to s2, return r1, otherwise return r2.
     *
     * @param s1
     * @param s2
     * @param r1
     * @param r2
     * @return String
     */
    public static String check4match(String s1, String s2, String r1, String r2) {
        return s1 == null ? "check4match s1 null" : //$NON-NLS-1$
                s2 == null ? "check4match s2 null" : //$NON-NLS-1$
                        r1 == null ? "check4match r1 null" : //$NON-NLS-1$
                                r2 == null ? "check4match r2 null" : //$NON-NLS-1$
                                        s1.equals(s2) ? r1 : r2;
    }

    /**
     * Return true if the passed String is not null or empty
     *
     * @param s
     *            String to test.
     * @return false if null or empty String, true otherwise
     */
    public static boolean isPopulated(String s) {
        return s != null && !EMPTY_STRING.equals(s);
    }

    public static String fillStringLeft(String str, int length, String filler) {
        StringBuffer sb = new StringBuffer();
        int m = str.length();
        while (m++ < length) {
            sb.append(filler);
        }
        sb.append(str);
        return sb.toString();
    }

    /**
     * pad a String with a given character. If str param is longer than width,
     * str will be returned un-changed.
     *
     * @param str
     *            String to be padded
     * @param fill
     *            char to pad the String with
     * @param width
     *            How wide the result String is to be
     * @param left
     *            Pad left if true, right if false
     * @return String padded left or right with fill char
     */
    public static String pad(String str, char fill, int width, boolean left) {
        StringBuffer sb = new StringBuffer();
        if (str == null) {
            str = EMPTY_STRING;
        }
        int l = str.length();
        if (l > (width - 1)) {
            return str;
        }
        synchronized (sb) {
            if (!left) {
                sb.append(str);
            }
            for (int i = l; i < width; i++) {
                sb.append(fill);
            }
            if (left) {
                sb.append(str);
            }
        }
        return sb.toString();
    }

    /**
     * centerInScreen returns a point that can be used in a window setLocation
     * call to place the window in the center of the screen.
     *
     * @param d
     *            For example, the Dimension of the window from a getSize()
     *            call.
     * @return Point Returns upper left corner point that would place the window
     *         in the center.
     */
    public static Point centerInScreen(final Dimension d) {
        final Dimension s = TOOLKIT.getScreenSize();
        return new Point(((s.width - d.width) / 2), ((s.height - d.height) / 2));
    }

    /**
     * Center a Dimension in a Rectangle.
     *
     * @param d
     *            Dimension to center
     * @param r
     *            Rectangle to center Dimension in
     * @return center of r
     */
    public static Point centerInRect(final Dimension d, final Rectangle r) {
        return new Point(r.x + ((int) (r.width - d.width) / 2), r.y
                + ((int) (r.height - d.height) / 2));
    }

    /**
     * Copy a JVM path resource to a file.
     *
     * @param resourcePath
     *            ie com/company/favicon.png
     * @param targetPath
     *            ie html/images/icon16x16.png. If called from Flywheel, the
     *            targetPath is relative to the target directory.
     */
    public static void copyLibraryResource(String resourcePath,
            String targetPath) throws IOException {
        copyLibraryResource(resourcePath, new File(targetPath));
    }

    /**
     * Copy a JVM path resource to a file.
     *
     * @param resourcePath
     * @param targetFile
     */
    public static void copyLibraryResource(String resourcePath, File targetFile)
            throws IOException {
        UtilIO.copyLibraryResource(resourcePath, targetFile);
    }

    /**
     * Create the parent directory structure for a file.
     *
     * @param file
     */
    protected static boolean createParentDirectories(File file) {
        return UtilIO.createParentDirectories(file);
    }

    /**
     * Save byte array to a file.
     *
     * @param bytes
     * @param file
     */
    public static void saveBytesToFile(byte[] bytes, File file) {
        UtilIO.saveBytesToFile(bytes, file);
    }

    /**
     * Your basic file copy,
     * http://forums.sun.com/thread.jspa?threadID=623784&tstart=225
     *
     * @param in
     * @param out
     */
    public static void copyFile(File in, File out) {
        UtilIO.copyFile(in, out);
    }

    /**
     * Your basic character file copy,
     * http://forums.sun.com/thread.jspa?threadID=623784&tstart=225
     *
     * @param in
     * @param out
     * @param encoding
     */
    public static void copyFile(File in, File out, String encoding) {
        UtilIO.copyFile(in, out, encoding);
    }

    /**
     * Sometimes you just have to copy a stream.
     *
     * @param is
     * @param os
     */
    public static void copyStream(InputStream inputStream,
            OutputStream outputStream) throws IOException {
        UtilIO.copyStream(inputStream, outputStream);
    }

    /**
     * Save a package resource from the classpath to a file.
     *
     * @param packagePath
     *            for example "/com/panopset/nls/flagicons.jar"
     * @param directoryPath
     * @return File where package resource is saved.
     */
    public static File savePackageResource(String packagePath,
            String directoryPath) throws IOException {
        return UtilIO.savePackageResource(packagePath, directoryPath);
    }

    /**
     * Get the URL for an application package resource.
     *
     * @param clazz
     *            Class that resides in the package the the resource is from.
     * @param resourceName
     *            Simple name of the resource.
     * @return URL for resource.
     */
    public static URL getPackageURL(final Class<?> clazz, String resourceName) {
        return clazz.getResource(convertPackageToURLsyntax(clazz.getPackage()
                .getName())
                + "/" + resourceName);
    }

    /**
     * Get the text of a file in the same package of a given file.
     *
     * @param clazz
     *            Class in same package where file resides.
     * @param name
     *            file name.
     * @return Text contents of file.
     */
    public static String getPackageText(Class<?> clazz, String name) {
        URL url = getPackageURL(clazz, name);
        if (url == null) {
            return x("Not found: ") + clazz.getCanonicalName() + name;
        }
        return getTextFromURL(url);
    }

    /**
     * Get the text from a file represented by a URL.
     *
     * @param url
     *            Location of text file.
     * @return Text contents of file.
     */
    public static String getTextFromURL(URL url) {
        StringBuffer sb = new StringBuffer();
        InputStreamReader isr = null;
        BufferedReader br = null;
        String s = null;
        try {
            isr = new InputStreamReader(url.openStream());
            br = new BufferedReader(isr);
            while ((s = br.readLine()) != null) {
                sb.append(s).append(getEol());
            }
        } catch (Exception e) {
            log(e);
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e1) {
                log(e1);
            }
            try {
                if (isr != null) {
                    isr.close();
                }
            } catch (IOException e2) {
                log(e2);
            }
        }
        return sb.toString();
    }

    /** Match period. */
    public static final Pattern REGEX_PATTERN_JAVA_PACKAGE = Pattern
            .compile(BACK_SLASH + PERIOD);

    /**
     * Convert package to URL syntax.
     *
     * @param s
     *            Dot separated package name.
     * @return Forward slash separated path.
     */
    public static String convertPackageToURLsyntax(final String s) {
        return FORWARD_SLASH
                + REGEX_PATTERN_JAVA_PACKAGE.matcher(s).replaceAll(
                        FORWARD_SLASH);
    }

    /**
     * Get an Integer from a property file, using the given key.
     *
     * @param p
     *            Properties file.
     * @param key
     * @return null if property not found or not convertable to an int,
     *         otherwise the int value.
     */
    public static Integer getIntegerFromProperty(Properties p, String key) {
        if (p == null) {
            return null;
        }
        if (key == null) {
            return null;
        }
        Object o = p.get(key);
        if (o == null) {
            return null;
        }
        String s = o.toString();
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            log(e);
            return null;
        }
    }

    private static int uniqueKey = 0;

    public static String generateUniqueKey() {
        return UNDERSCORE + uniqueKey++;
    }

    /**
     * Generate a unique key independent of run time, based on stack trace.
     *
     * @return String "key_" + md5sum(getStackTrace)
     */
    public static String generateStackHashKey() {
        Exception e = null;
        try {
            throw new Exception();
        } catch (Exception ex) {
            e = ex;
        }
        return "key_" + UtilMD5.md5sum(getStackTrace(e));
    }

    public static String getStackTrace(Throwable t) {
        StringWriter sw = new StringWriter();
        try {
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            pw.flush();
            pw.close();
            sw.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return sw.toString();
    }

    /**
     * Validate that a file exists, and it is not a directory.
     *
     * @param file
     * @return boolean true if valid file found
     */
    public static boolean validateFile(final File file) {
        return ((validateExistence(file)) && (file.isFile()));
    }

    /**
     * Validate that a file exists, and that it is a directory.
     *
     * @param file
     * @return boolean true if valid directory found
     */
    public static boolean validateDirectory(final File file) {
        if (validateExistence(file)) {
            if (file.isDirectory()) {
                return true;
            } else {
                dspmsg(x("File") + SINGLE_SPACE + getCanonicalPath(file)
                        + SINGLE_SPACE + x("is not a directory"));
            }
        } else {
            return false;
        }
        return ((validateExistence(file)) && (file.isDirectory()));
    }

    private static boolean validateExistence(final File file) {
        if (file == null) {
            dspmsg(x("file is null"));
            return false;
        }
        if (file.exists()) {
            return true;
        } else {
            dspmsg(x("file") + SINGLE_SPACE + getCanonicalPath(file)
                    + SINGLE_SPACE + x("does not exist"));
            return false;
        }
    }

    /**
     * Get the String page source of an http page.
     *
     * @param urlStr
     * @return page source String.
     * @throws MalformedURLException
     * @throws IOException
     */
    public static String httpGet(String urlStr) throws Exception {
        StringBuffer sb = new StringBuffer();
        try {
            URL site = new URL(urlStr);
            HttpURLConnection conn = (HttpURLConnection) site.openConnection();
            conn.setReadTimeout(getConnectTimeout());
            InputStream in = conn.getInputStream();
            byte buff[] = new byte[4096];
            int count = 0;
            synchronized (sb) {
                while ((count = in.read(buff)) > 0) {
                    sb.append(new String(buff, 0, count));
                }
            }
            in.close();
        } catch (java.net.SocketTimeoutException ste) {
            dspmsg("WARNING: timed out after " + getConnectTimeoutString()
                    + " milliseconds");
        }
        return sb.toString();
    }

    /**
     * Default connect timeout is 25000 (25 seconds).
     */
    private static final Integer CONNECT_TIMEOUT_DEFAULT = 25000;

    /**
     * Connect timeout.
     */
    private static Integer connectTimeout = CONNECT_TIMEOUT_DEFAULT;

    /**
     * Get connection timeout. Default is 25000 (25 seconds).
     *
     * @return timeout, in milliseconds.
     */
    public static Integer getConnectTimeout() {
        return connectTimeout;
    }

    /**
     * Set the connection timeout.
     *
     * @param value
     *            in milliseconds.
     */
    public static void setConnectTimeout(int value) {
        connectTimeout = value;
        connectTimeoutString = null;
    }

    /**
     * Connection timeout.
     */
    private static String connectTimeoutString;
    public static FlexFrame parentFrame;

    /**
     * New dialogs sometimes need a parent frame. One will be created if one
     * hasn't been registered by extending AbstractApplication.
     *
     * @return parent frame
     */
    public static JFrame getParentFrame() {
        if (parentFrame != null) {
            return parentFrame.getFrame();
        }
        return new JFrame();
    }

    /**
     * getConnectionTimeout as a String.
     *
     * @return String representation of getConnectTimeout.
     */
    public static String getConnectTimeoutString() {
        if (connectTimeoutString == null) {
            connectTimeoutString = EMPTY_STRING + getConnectTimeout();
        }
        return connectTimeoutString;
    }

    public static void extractZipFile(File dir, BufferedInputStream bis) {
        try {

            mkdirs(dir);

            if (!dir.isDirectory()) {
                return;
            }

            ZipInputStream zis = new ZipInputStream(bis);
            ZipEntry ze = zis.getNextEntry();

            while (ze != null) {

                if (ze.isDirectory()) {
                    ze = zis.getNextEntry();
                    continue;
                }

                File f = new File(dir + "/" + ze.getName());

                mkdirs(f.getParentFile());

                copyOpenStream(new BufferedInputStream(zis),
                        new BufferedOutputStream(new FileOutputStream(f)));

                zis.closeEntry();

                ze = zis.getNextEntry();
            }

            zis.close();

        } catch (Exception e) {
            log(e);
        }
    }

    public static void copyOpenStream(BufferedInputStream bis,
            BufferedOutputStream bos) {
        boolean done = false;
        try {
            synchronized (bis) {
                synchronized (bos) {
                    byte[] buff = new byte[4096];
                    while (done == false) {
                        int c = bis.read(buff);
                        if (c == -1) {
                            done = true;
                        } else {
                            bos.write(buff, 0, c);
                        }
                    }
                }
            }
            bos.flush();
            bos.close();
        } catch (IOException ex) {
            log(ex);
        }
    }

    public static Image getImageFromFile(File f) {
        try {
            Image rtn = null;
            if (f == null) {
                log("file is null");
                return null;
            }
            if (!f.exists()) {
                log(getCanonicalPath(f) + " does not exist.");
                return null;
            }
            if (!f.canRead()) {
                log(getCanonicalPath(f)
                        + " failed to read due to operating system permissions issue.");
                return null;
            }
            if (!checkMemory(f.length())) {
                dspmsg(x("Out of memory, please check log for details."));
            }
            dspmsg("loading " + f.getName() + " ...");
            rtn = ImageIO.read(f);
            dspmsg(f.getName() + " loaded.");
            return rtn;
        } catch (IOException e) {
            dspmsg(e.getMessage());
            log(e);
            return null;
        }
    }

    /**
     * If you know how much memory is required for an operation, pass that
     * amount in bytes to this method.
     *
     * @param needed
     *            How much memory is needed.
     * @return true iff enough memory is available.
     */
    public static boolean checkMemory(long needed) {
        long fm = getFreeMemory();
        if (needed > fm) {
            int length = maxWidth(fm, needed);
            log(x("Available memory: ") + fillStringLeft("" + fm, length, ZERO));
            log(x("Memory needed   : ")
                    + fillStringLeft("" + needed, length, ZERO));
            dspmsg(x("Not enough memory, try increasing memory, for example:"));
            dspmsg("-Xms256m -Xmx1024m");
            dspmsg(x("sets minimum to 256 megabytes and maximum to about a gig"));
            return false;
        }
        return true;
    }

    /**
     * Returns the maximum length of two objects, after conversion to String.
     * Primitives may be passed in due to the Java 5 autoboxing feature. If both
     * are null, -1 is returned.
     *
     * @param o0
     *            Object 0
     * @param o1
     *            Object 1
     * @return
     */
    public static int maxWidth(Object o0, Object o1) {
        if (o0 == null && o1 == null) {
            return -1;
        }
        if (o0 == null) {
            return ("" + o1).length();
        }
        if (o1 == null) {
            return ("" + o0).length();
        }
        return Math.max(("" + o0).length(), ("" + o1).length());
    }

    public static long getFreeMemory() {
        return Runtime.getRuntime().freeMemory();
    }

    public static long getTotalMemory() {
        return Runtime.getRuntime().totalMemory();
    }

    public static long getMaxMemory() {
        return Runtime.getRuntime().maxMemory();
    }

    public static String getStringFromUser(String prompt) {
        JTextField tf = new JTextField(20);
        TPanel cp = new TPanel.Builder().layout(new FlowLayout()).construct();
        cp.add(tf);
        new TypicalDialog.Builder(prompt).centerPanel(cp).size(
                new Dimension(300, 80)).construct().getFrame().setVisible(true);
        return tf.getText();
    }

    public static void saveCanvas2file(File f, JPanel c) {
        Dimension sz = c.getSize();
        final BufferedImage image = new BufferedImage(sz.width, sz.height,
                BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        c.paint(g);
        saveFinishedImage2file(f, image);
    }

    public static void saveCanvas2file(File f, Canvas c) {
        Dimension sz = c.getSize();
        final BufferedImage image = new BufferedImage(sz.width, sz.height,
                BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        c.paint(g);
        saveFinishedImage2file(f, image);
    }

    public static void saveImage2file(File f, Image i) {
        final BufferedImage image = new BufferedImage(i.getWidth(null), i
                .getHeight(null), BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        g.drawImage(i, 0, 0, null);
        saveFinishedImage2file(f, image);
    }

    public static void saveFinishedImage2file(File f, BufferedImage bi) {
        String path = f.getPath();
        String fileType = path.substring(path.lastIndexOf(".") + 1);
        assert fileType != null && fileType.length() > 0;
        try {
            ImageIO.write(bi, fileType, f);
        } catch (IOException e) {
            dspmsg("Check permissions to " + getCanonicalPath(f));
            log(e);
        }
    }

    public static File getDirectoryFromUser(File path, String prompt) {
        return getFileFromUser(new JFrame(), path, prompt,
                JFileChooser.DIRECTORIES_ONLY, JFileChooser.OPEN_DIALOG);
    }

    public static File getDirectoryFromUser(JFrame parentFrame, File path,
            String prompt) {
        return getFileFromUser(parentFrame, path, prompt,
                JFileChooser.DIRECTORIES_ONLY, JFileChooser.OPEN_DIALOG);
    }

    public static File getFileOrDirectoryFromUser(JFrame parentFrame,
            File path, final String prompt, int dialogType) {
        return getFileFromUser(parentFrame, path, prompt,
                JFileChooser.FILES_AND_DIRECTORIES, dialogType);
    }

    public static File getFileFromUser(JFrame parentFrame, File path,
            final String prompt, int dialogType) {
        return getFileFromUser(parentFrame, path, prompt,
                JFileChooser.FILES_ONLY, dialogType);
    }

    static File dftPath = new File(System.getProperty("user.home"));

    public static File getFileFromUser(JFrame parentFrame, File path,
            String prompt, int mode, int dialogType) {
        File rtn = null;
        JFileChooser chooser = new JFileChooser();
        try {
            if ((dftPath != null) && (dftPath.getCanonicalPath().length() > 0)) {
                chooser.setSelectedFile(dftPath);
            }
        } catch (IOException ex) {
            log(ex);
            return null;
        }
        chooser.setFileSelectionMode(mode);
        chooser.setDialogTitle(prompt);
        chooser.setDialogType(dialogType);
        if (chooser.showOpenDialog(parentFrame) == JFileChooser.APPROVE_OPTION) {
            rtn = chooser.getSelectedFile();
            dftPath = rtn.getParentFile();
        }

        return rtn;
    }

    public static char getAvailableMnemonic(String category, String s, char c) {
        // TODO NLS plug-in.
        return c;
    }

    /**
     * Given a Vector of Strings, return a Vector of Strings with any extensions
     * dropped off. ie: x.txt becomes x.
     *
     * @param v
     * @return Vector<String>
     */
    public static Vector<String> dropExtensions(Vector<String> v) {
        Vector<String> rtn = new Vector<String>();
        for (String s : v) {
            rtn.add(dropExtension(s));
        }
        return rtn;
    }

    /**
     * Drop path from String, /foo/bar becomes bar.
     *
     * @param s
     *            If empty or null, an empty String will be returned.
     * @return String with paths dropped off.
     */
    public static String dropPath(String s) {
        if (!isPopulated(s)) {
            return EMPTY_STRING;
        }
        int i = s.lastIndexOf(FORWARD_SLASH);
        return (i > -1 && s.length() > i) ? s.substring(i + 1) : s;
    }

    /**
     * Drop extension from String, x.txt becomes x.
     *
     * @param s
     *            If empty or null, an empty String will be returned.
     * @return String with extension dropped off.
     */
    public static String dropExtension(String s) {
        if (!isPopulated(s)) {
            return EMPTY_STRING;
        }
        int i = s.lastIndexOf(PERIOD);
        return (i > -1) ? s.substring(0, i) : s;
    }

    public static String getExtension(File f) {
        return getExtension(getCanonicalPath(f));
    }

    public static String getExtension(String s) {
        if (isPopulated(s)) {
            int i = s.lastIndexOf(PERIOD);
            if (i > -1) {
                return s.substring(i);
            }
        }
        return EMPTY_STRING;
    }

    /**
     * Get property value from a String representation of a property as stored
     * as a Property file line.
     *
     * @param src
     *            String representation of a property as stored, ie: a=b
     * @return Everything after the = sign, or an empty String object if src is
     *         null or not in a property format.
     */
    public static String getPropFromStringRep(String src) {
        if (isPopulated(src)) {
            int i = src.indexOf("=");
            if (i > -1) {
                String spliced = src.substring(i);
                if (spliced.length() > 1) {
                    return spliced.substring(1);
                }
            }
        }
        return EMPTY_STRING;
    }

    public static String getKeyFromStringRep(String src, String sep) {
        if (isPopulated(src)) {
            int i = src.indexOf(sep);
            if (i > -1) {
                return (src.substring(0, i));
            }
        }
        return EMPTY_STRING;
    }

    /**
     * ie: changeExtensions(new File("/home/app/nls"),".properties", ".txt")
     *
     * @param directory
     *            Directory to change extensions in
     * @param oldExtension
     *            Only files with this extension will be changed. If blank or
     *            null, all files affected.
     * @param newExtension
     *            Affected files will get this new extension, with the old
     *            extension, if any, dropped.s
     */
    public static void changeExtensions(File directory, String oldExtension,
            String newExtension) {
        for (File f : directory.listFiles()) {
            if (f.isDirectory()) {
                changeExtensions(f, oldExtension, newExtension);
            } else {
                changeExtension(f, oldExtension, newExtension);
            }
        }
    }

    /**
     * ie: changeExtension(new
     * File("/home/app/nls/en.properties"),".properties", ".txt")
     *
     * @param f
     *            File to change extensions in
     * @param oldExtension
     * @param newExtension
     */
    public static void changeExtension(File f, String oldExtension,
            String newExtension) {
        if (!isPopulated(oldExtension)
                || oldExtension.equals(getExtension(f.getName()))) {
            File n = new File(dropExtension(getCanonicalPath(f)) + "/"
                    + newExtension);
            copyFile(f, n);
            delete(f);
        }
    }

    /**
     * Prevent instantiation.
     */
    private Util() {
    }
}
